#ifndef _MATHEMATICS_ALGEBRA_TERM_
#define _MATHEMATICS_ALGEBRA_TERM_

#include "..\..\utilities\string_helper.h"
using namespace science::utilities;

#include <vector>
#include <map>
#include <iostream>
#include <math.h>

namespace science
{
	namespace mathematics
	{
		namespace algebra
		{
			class term
			{
			private:
				double _coeffiecient;
				std::map<char, double> _variable_powers;
				bool _lead_term;

			public:
				term()
				{
					_coeffiecient = 0;
					_variable_powers.clear();
					_lead_term = false;
				}

				term(double coeffiecient)
				{
					_coeffiecient = coeffiecient;
					_variable_powers.clear();
					_lead_term = false;
				}

				term(double coeffiecient, char var_name, double variable_power)
				{
					_coeffiecient = coeffiecient;
					_variable_powers[var_name] = variable_power;
					_lead_term = false;
				}

				term(double coeffiecient, std::map<char, double> variable_powers)
				{
					_coeffiecient = coeffiecient;
					_variable_powers = variable_powers;
					_lead_term = false;
				}

				term(std::string term_str)
				{
					unsigned int index = 0;
					*this = parse_term(term_str, index);
				}

				~term()
				{
					_variable_powers.clear();
				}

			public:

				double evaluate(char var_name, double var_value)
				{
					if (_variable_powers.size() > 1)
						throw new std::invalid_argument("Not enough values to evaluate term");

					if (_variable_powers.find(var_name) == _variable_powers.end())
						throw new std::invalid_argument("Not a variable in the term");

					double res = _coeffiecient * pow(var_value, _variable_powers[var_name]);
					return res;
				}

				double evaluate(std::map<char, double> var_values)
				{
					if (var_values.size() < _variable_powers.size())
						throw new std::invalid_argument("Not enough values to evaluate term");

					double res = _coeffiecient;
					for (auto entry : var_values)
					{
						if (_variable_powers.find(entry.first) == _variable_powers.end())
							continue;/*throw new std::invalid_argument("Not a variable in the term");*/

						res *= pow(entry.second, _variable_powers[entry.first]);
					}

					return res;
				}

				unsigned int get_variables_count() const { return _variable_powers.size(); }

				bool like(const term& t)
				{
					if (t._variable_powers.size() != _variable_powers.size())
						return false;

					for (auto entry : t._variable_powers)
						if (math_helper::double_check(entry.second, _variable_powers[entry.first]) != 0)
							return false;
					return true;
				}

				void set_lead_term(bool set_lead = true) { _lead_term = set_lead; }

				void resize_powers(const term& t)
				{		
					for (auto entry : t._variable_powers)
					{
						if (_variable_powers.find(entry.first) == _variable_powers.end())
							_variable_powers[entry.first] = 0;
					}
				}

			public:

				bool operator==(const term& t)
				{
					return (const_cast<const term*>(this))->operator==(t);
				}

				bool operator!=(const term& t)
				{
					return (const_cast<const term*>(this))->operator!=(t);
				}

				bool operator==(const term& t) const
				{
					term t_temp = t;
					term this_temp = *this;
					bool equals = this_temp._coeffiecient == t_temp._coeffiecient;
					equals &= (t_temp._variable_powers.size() == this_temp._variable_powers.size());
					for (auto entry : t._variable_powers)
					{
						if (_variable_powers.find(entry.first) == _variable_powers.end())
							return false;
						equals &= math_helper::double_check(entry.second, this_temp._variable_powers[entry.first]) == 0;
					}
					return equals;
				}

				bool operator!=(const term& t) const
				{
					return !(this->operator==(t));
				}

				term& operator*=(const term& rhs)
				{
					*this = this->operator*(rhs);
					return *this;
				}

				term operator*(const term& rhs)
				{
					return (const_cast<const term*>(this))->operator*(rhs);
				}

				term operator*(const term& rhs) const
				{
					term max_power = rhs._variable_powers.size() > _variable_powers.size() ? rhs : *this;
					term t = *this;
					term t2 = rhs;
					t.resize_powers(max_power);
					t2.resize_powers(max_power);

					t._coeffiecient *= t2._coeffiecient;
					for (auto& entry : t._variable_powers)
						entry.second = entry.second + t2._variable_powers[entry.first];
					return t;
				}

				term& operator*=(double rhs)
				{
					*this = this->operator*(rhs);
					return *this;
				}

				term operator*(double rhs)
				{
					return (const_cast<const term*>(this))->operator*(rhs);
				}

				term operator*(double rhs) const
				{
					term t = *this;
					t._coeffiecient *= rhs;
					return t;
				}

				term& operator/=(double rhs)
				{
					*this = this->operator/(rhs);
					return *this;
				}

				term operator/(double rhs)
				{
					return (const_cast<const term*>(this))->operator/(rhs);
				}

				term operator/(double rhs) const
				{
					if (math_helper::double_check(rhs, 0) == 0)
						throw new std::invalid_argument("Cannot divide by zero");

					term t = *this;
					t._coeffiecient /= rhs;
					return t;
				}

				term& operator/=(const term& rhs)
				{
					*this = this->operator*(rhs);
					return *this;
				}

				term operator/(const term& rhs)
				{
					return (const_cast<const term*>(this))->operator*(rhs);
				}

				term operator/(const term& rhs) const
				{
					if (rhs._coeffiecient < DBL_EPSILON)
						throw new std::invalid_argument("Cannot divide by zero");

					term max_power = rhs._variable_powers.size() > _variable_powers.size() ? rhs : *this;
					term t = *this;
					term t2 = rhs;
					t.resize_powers(max_power);
					t2.resize_powers(max_power);

					t._coeffiecient /= t2._coeffiecient;
					for (auto& entry : t._variable_powers)
						entry.second = entry.second - t2._variable_powers[entry.first];
					return t;
				}

			public:

				double get_coeffiecient() const { return _coeffiecient; }
				double get_variable_power(char var_name) { return _variable_powers[var_name]; }
				void set_coeffiecient(double coeffiecient) { _coeffiecient = coeffiecient; }
				void set_variable_power(double variable_power, char var_name)
				{ 
					_variable_powers[var_name] = variable_power;
				}

			public:
				friend std::ostream &operator<<(std::ostream &output, const term &exp)
				{
					bool unit_coefficient = false;

					if (math_helper::double_check(exp.get_coeffiecient(), 0) == -1)
					{
						if (math_helper::double_check(exp.get_coeffiecient(), -1) != 0)
						{
							if (exp._lead_term)
								output << "-" << abs(exp.get_coeffiecient());
							else
								output << " - " << abs(exp.get_coeffiecient());
						}
						else
							unit_coefficient = true;
					}
					else if (math_helper::double_check(exp.get_coeffiecient(), 0) == 1)
					{
						if (math_helper::double_check(exp.get_coeffiecient(), 1) != 0)
						{
							if (!exp._lead_term)
								output << " + ";
							output << exp.get_coeffiecient();
						}
						else
							unit_coefficient = true;
					}

					if (math_helper::double_check(exp.get_coeffiecient(), 0) != 0)
					{
						if (unit_coefficient)
						{
							if (math_helper::double_check(exp.get_coeffiecient(), 0) == -1)
							{
								if (exp._lead_term)
									output << "-";
								else
									output << " - ";
							}
							else
							{
								if (!exp._lead_term)
									output << " + ";
							}
						}

						bool found = false;
						for (auto& entry : exp._variable_powers)
						{
							if (math_helper::double_check(entry.second, 0) != 0)
							{
								output << entry.first;
								if (math_helper::double_check(abs(entry.second), 1) != 0)
									output << "^" << entry.second;
								found = true;
							}
						}
						if (!found && unit_coefficient)
							output << exp.get_coeffiecient();
					}

					return output;
				}

				friend std::istream &operator>>(std::istream  &input, term &exp)
				{
					unsigned int powers_count = 0;
					std::cout << "Enter coefficient : ";
					input >> exp._coeffiecient;
					std::cout << "Enter powers count : ";
					input >> powers_count;
					if (powers_count > 0)
					{
						for (unsigned int i = 0; i < powers_count; i++)
						{
							std::cout << "Enter variable and power (" << i << ") : ";
							double power = 0; char var_name = '\0';
							input >> var_name >> power;
							exp._variable_powers[var_name] = power;
						}
					}
					return input;
				}

			public:
				static bool parse_variable(std::string str, unsigned int& index, char& var_name, double& power)
				{
					if (index >= str.size())
						return false;

					if (string_helper::is_numeric_double(str[index]) || string_helper::is_sign(str[index]) != 0)
						return false;

					if (str[index] == '=' || str[index] == '>' || str[index] == '<')
						return false;

					if (str[index] == '(' || str[index] == ')')
						return false;

					if (str[index] < 'A' || str[index] > 'z')
						throw new std::invalid_argument("Invalid variable string");

					var_name = str[index]; index++;
					power = 1.0;

					if (index >= str.size())
						return true;

					if (str[index] == '^')
					{
						index++;
						if (index >= str.size())
							throw new std::invalid_argument("Invalid variable string");
						power = string_helper::parse_double(str, index);
					}
					return true;
				}

				static term parse_term(std::string str, unsigned int& index)
				{
					str = string_helper::remove_white_chars(str);
					term t;

					if (index >= str.size())
						return t;

					t.set_coeffiecient(string_helper::parse_double(str, index));

					if (index >= str.size())
						return t;

					char var_name;
					double power;
					while (parse_variable(str, index, var_name, power))
						t.set_variable_power(power, var_name);
					return t;
				}
			};

			static term operator*(double num, const term& rhs)
			{
				return rhs.operator*(num);
			}

			static term operator/(double num, const term& rhs)
			{
				return rhs.operator/(num);
			}
		}
	}
}

#endif